<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">

    <title>Fresnel Shader — Alien.js</title>

    <link rel="preconnect" href="https://fonts.gstatic.com">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto+Mono">
    <link rel="stylesheet" href="../assets/css/style.css">

    <script type="module">
        import { Color, ColorManagement, GLSL3, Group, LinearSRGBColorSpace, Mesh, OrbitControls, PanelItem, PerspectiveCamera, RawShaderMaterial, Scene, SphereGeometry, UI, Vector2, WebGLRenderer, brightness, getFullscreenTriangle, ticker } from '../../build/alien.three.js';

        // Based on https://oframe.github.io/ogl/examples/?src=fresnel.html by michaeldll

        const colors = {
            backgroundColor: 0x060606,
            baseColor: 0x060606,
            fresnelColor: 0x3b5b89
        };

        import periodic3d from '../../src/shaders/modules/noise/periodic3d.glsl.js';

        class FresnelMaterial extends RawShaderMaterial {
            constructor() {
                super({
                    glslVersion: GLSL3,
                    uniforms: {
                        uBaseColor: { value: new Color(colors.baseColor) },
                        uFresnelColor: { value: new Color(colors.fresnelColor) },
                        uFresnelPower: { value: 1.5 },
                        uTime: WorldController.time
                    },
                    vertexShader: /* glsl */ `
                        in vec3 position;
                        in vec3 normal;

                        uniform mat4 modelMatrix;
                        uniform mat4 modelViewMatrix;
                        uniform mat4 projectionMatrix;
                        uniform vec3 cameraPosition;

                        uniform float uTime;

                        out vec3 vWorldNormal;
                        out vec3 vViewDirection;

                        ${periodic3d}

                        void main() {
                            // http://connorbell.ca/2017/09/09/Generating-Looping-Noise.html

                            float loopLength = 2.0;
                            float transitionStart = 1.5;
                            float time = mod(uTime, loopLength);

                            float v1 = 0.05 * pnoise(vec3(2.0 * normal + time), vec3(10.0));
                            float v2 = 0.05 * pnoise(vec3(2.0 * normal + time - loopLength), vec3(10.0));

                            float transitionProgress = (time - transitionStart) / (loopLength - transitionStart);
                            float progress = clamp(transitionProgress, 0.0, 1.0);

                            float result = mix(v1, v2, progress);

                            vec3 worldPosition = (modelMatrix * vec4(position + result * normal, 1.0)).xyz;
                            vWorldNormal = normalize(modelMatrix * vec4(normal, 0.0)).xyz;
                            vViewDirection = normalize(cameraPosition - worldPosition);

                            gl_Position = projectionMatrix * modelViewMatrix * vec4(position + result * normal, 1.0);
                        }
                    `,
                    fragmentShader: /* glsl */ `
                        precision highp float;

                        uniform vec3 uBaseColor;
                        uniform vec3 uFresnelColor;
                        uniform float uFresnelPower;

                        in vec3 vWorldNormal;
                        in vec3 vViewDirection;

                        out vec4 FragColor;

                        void main() {
                            float fresnelFactor = abs(dot(vViewDirection, vWorldNormal));
                            float inversefresnelFactor = 1.0 - fresnelFactor;

                            // Shaping function
                            fresnelFactor = pow(fresnelFactor, uFresnelPower);
                            inversefresnelFactor = pow(inversefresnelFactor, uFresnelPower);

                            FragColor = vec4(fresnelFactor * uBaseColor + inversefresnelFactor * uFresnelColor, 1.0);
                        }
                    `
                });
            }
        }

        class SceneView extends Group {
            constructor() {
                super();

                this.initMesh();
            }

            initMesh() {
                this.mesh = new Mesh(
                    new SphereGeometry(1, 160, 80),
                    new FresnelMaterial()
                );
                this.add(this.mesh);
            }
        }

        class PanelController {
            static init(scene, view, ui) {
                this.scene = scene;
                this.view = view;
                this.ui = ui;

                this.lastInvert = null;

                this.initPanel();
                this.setInvert(this.scene.background);
            }

            static initPanel() {
                const { mesh } = this.view;

                const items = [
                    {
                        name: 'FPS'
                    },
                    {
                        type: 'divider'
                    },
                    {
                        type: 'color',
                        value: this.scene.background,
                        callback: value => {
                            this.scene.background.copy(value);

                            this.setInvert(value);
                        }
                    },
                    {
                        type: 'divider'
                    },
                    {
                        type: 'color',
                        value: mesh.material.uniforms.uBaseColor.value,
                        callback: value => {
                            mesh.material.uniforms.uBaseColor.value.copy(value);
                        }
                    },
                    {
                        type: 'color',
                        value: mesh.material.uniforms.uFresnelColor.value,
                        callback: value => {
                            mesh.material.uniforms.uFresnelColor.value.copy(value);
                        }
                    },
                    {
                        type: 'slider',
                        name: 'Power',
                        min: 0,
                        max: 3,
                        step: 0.01,
                        value: mesh.material.uniforms.uFresnelPower.value,
                        callback: value => {
                            mesh.material.uniforms.uFresnelPower.value = value;
                        }
                    }
                ];

                items.forEach(data => {
                    this.ui.addPanel(new PanelItem(data));
                });
            }

            // Public methods

            static setInvert = value => {
                // Light colour is inverted
                const invert = brightness(value) > 0.6;

                if (invert !== this.lastInvert) {
                    this.lastInvert = invert;

                    this.ui.invert(invert);
                }
            };
        }

        class RenderManager {
            static init(renderer, scene, camera) {
                this.renderer = renderer;
                this.scene = scene;
                this.camera = camera;
            }

            // Public methods

            static resize = (width, height, dpr) => {
                this.renderer.setPixelRatio(dpr);
                this.renderer.setSize(width, height);
            };

            static update = () => {
                this.renderer.render(this.scene, this.camera);
            };
        }

        class WorldController {
            static init() {
                this.initWorld();
                this.initControls();

                this.addListeners();
            }

            static initWorld() {
                this.renderer = new WebGLRenderer({
                    powerPreference: 'high-performance',
                    antialias: true
                });

                // Output canvas
                this.element = this.renderer.domElement;

                // Disable color management
                ColorManagement.enabled = false;
                this.renderer.outputColorSpace = LinearSRGBColorSpace;

                // 3D scene
                this.scene = new Scene();
                this.scene.background = new Color(colors.backgroundColor);
                this.camera = new PerspectiveCamera(30);
                this.camera.near = 0.5;
                this.camera.far = 40;
                this.camera.position.z = 8;
                this.camera.lookAt(this.scene.position);

                // Global geometries
                this.screenTriangle = getFullscreenTriangle();

                // Global uniforms
                this.resolution = { value: new Vector2() };
                this.texelSize = { value: new Vector2() };
                this.aspect = { value: 1 };
                this.time = { value: 0 };
                this.frame = { value: 0 };
            }

            static initControls() {
                this.controls = new OrbitControls(this.camera, this.renderer.domElement);
                this.controls.enableDamping = true;
            }

            static addListeners() {
                this.renderer.domElement.addEventListener('touchstart', this.onTouchStart);
            }

            // Event handlers

            static onTouchStart = e => {
                e.preventDefault();
            };

            // Public methods

            static resize = (width, height, dpr) => {
                this.camera.aspect = width / height;
                this.camera.updateProjectionMatrix();

                if (width < height) {
                    this.camera.position.z = 10;
                } else {
                    this.camera.position.z = 8;
                }

                width = Math.round(width * dpr);
                height = Math.round(height * dpr);

                this.resolution.value.set(width, height);
                this.texelSize.value.set(1 / width, 1 / height);
                this.aspect.value = width / height;
            };

            static update = (time, delta, frame) => {
                this.time.value = time;
                this.frame.value = frame;

                this.controls.update();
            };
        }

        class App {
            static async init() {
                this.initWorld();
                this.initViews();
                this.initControllers();

                this.addListeners();
                this.onResize();

                this.initPanel();
            }

            static initWorld() {
                WorldController.init();
                document.body.appendChild(WorldController.element);
            }

            static initViews() {
                this.view = new SceneView();
                WorldController.scene.add(this.view);

                this.ui = new UI({ fps: true });
                this.ui.animateIn();
                document.body.appendChild(this.ui.element);
            }

            static initControllers() {
                const { renderer, scene, camera } = WorldController;

                RenderManager.init(renderer, scene, camera);
            }

            static initPanel() {
                const { scene } = WorldController;

                PanelController.init(scene, this.view, this.ui);
            }

            static addListeners() {
                window.addEventListener('resize', this.onResize);
                ticker.add(this.onUpdate);
                ticker.start();
            }

            // Event handlers

            static onResize = () => {
                const width = document.documentElement.clientWidth;
                const height = document.documentElement.clientHeight;
                const dpr = window.devicePixelRatio;

                WorldController.resize(width, height, dpr);
                RenderManager.resize(width, height, dpr);
            };

            static onUpdate = (time, delta, frame) => {
                WorldController.update(time, delta, frame);
                RenderManager.update(time, delta, frame);
                this.ui.update();
            };
        }

        App.init();
    </script>
</head>
<body>
</body>
</html>
